classdef dotsTheComputablesManager < dotsAllRemoteObjectManagers
    % @class dotsTheComputablesManager
    % Coordinate dotsComputable* objects and topsDataLog.
    %
    % @ingroup computables
    
    properties
        % string name of managedObjects group to recompute automatically
        % @details
        % Use the activateGroup() method instead of normal property
        % setting, for two reasons:
        %   - allows correct behavior for the remote counterpart
        %   - easier to set with an anonymous function or fevalable
        % .
        activeGroup;
        
        % cell array of dotsComputable objects corresponding to activeGroup
        activeComputables;
        
        % time from the most recent data log update
        % @details
        % sendDataLogUpdate() updates latestLogUpdateTime each time it
        % sends a batch data from the client-side topsDataLog to the
        % server-side topsDataLog.
        % @details
        % latestLogUpdateTime is in units of the clockFunction of
        % topsDataLog.
        latestLogUpdateTime;
        
        % how often to save topsDataLog to disk
        % @details
        % writeLogToDiskInterval should be in units of clockFunction.  If
        % finite and non-negative, will cause periodic writing of
        % topsDataLog to disk with writeLogToDiskFileName.
        writeLogToDiskInterval;
        
        % file name for writing topsDataLog to disk
        writeLogToDiskFileName;
        
        % shared path for Snow Dots data
        dataPath;
    end
    
    properties (Hidden)
        % return value from an applyDataLogUpdate transacton
        latestLogUpdateLength;
        
        % next time when topsDataLog should be written to disk
        nextWriteLogToDiskTime;
    end
    
    methods (Access=private)
        % Constructor is private.
        % @details
        % Use dotsTheComputablesManager.theObject to access the current
        % instance.
        function self = dotsTheComputablesManager
            dotsTheMachineConfiguration.applySettingsToObject(self, true);
            dotsTheSwitchboard.sharePropertyOfObject('dataPath', self);
            self.initialize;
        end
    end
    
    methods (Static=true)
        % Access the current instance.
        function obj = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                constructor = str2func(mfilename);
                self = feval(constructor);
            end
            obj = self;
        end
        
        % Restore the current instance to a fresh state.
        function reset(varargin)
            factory = str2func([mfilename, '.theObject']);
            self = feval(factory);
            self.set(varargin{:});
            self.initialize;
        end
        
        % Launch a graphical interface to view managed dotsComputable
        % objects.
        function g = gui
            self = dotsTheComputablesManager.theObject;
            g = self.managedObjects.gui;
        end
        
        % Update topsDataLog on the server counterpart.
        % @details
        % Reads recent data from topsDataLog on the client manager's
        % machine, and sends the data as an update to the server
        % counterpart.
        % @details
        % If the topsDataLog on the client-side is empty, informs the
        % server counterpart so that it may flush topsDataLog on the server
        % side.
        function sendDataLogUpdate()
            self = dotsTheComputablesManager.theObject;
            
            [dataStruct, logWasFlushed] = self.getDataLogUpdate;
            if self.clientMode && self.mayRequestTransaction;
                
                % may flush the server-side log
                doFlush = logWasFlushed && ~self.serverMode;
                self.requestTransaction([], ...
                    'latestLogUpdateLength', true, ...
                    'applyDataLogUpdate', {[], doFlush});
                
                % send log entries one at a time,
                %   so each may fit in a UDP datagram
                for ii = 1:numel(dataStruct)
                    entry = dataStruct(ii);
                    self.requestTransaction([], ...
                        'latestLogUpdateLength', true, ...
                        'applyDataLogUpdate', {entry, false});
                end
                
            else
                % never need to flush client's log from the client side.
                doFlush = false;
                self.latestLogUpdateLength = ...
                    self.applyDataLogUpdate(dataStruct, doFlush);
            end
        end
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.initialize@dotsAllRemoteObjectManagers;
            
            self.mayRequestTransaction = false;
            
            self.activateGroup('none');
            self.latestLogUpdateTime = -inf;
            self.latestLogUpdateLength = nan;
            self.nextWriteLogToDiskTime = -inf;
            
            self.mayRequestTransaction = true;
        end
        
        % Carry out one transaction and recompute active objects, as
        % needed.
        % @details
        % Redefines the runBriefly() method of dotsAllRemoteObjectManagers
        % to also invoke mayComputeNow() on active objects and periodically
        % write topsDataLog to disk.
        function runBriefly(self)
            if self.serverMode || ~self.clientMode
                currentTime = feval(self.clockFunction);
                
                % periodic compute() for active computables
                objects = self.activeComputables;
                for ii = 1:length(objects)
                    [didCompute, result] = ...
                        objects{ii}.mayComputeNow(currentTime);
                end
                
                % periodic writing of topsDataLog to disk
                if ~isempty(self.writeLogToDiskFileName) ...
                        && isfinite(self.writeLogToDiskInterval) ...
                        && self.writeLogToDiskInterval >= 0 ...
                        && currentTime > self.nextWriteLogToDiskTime
                    
                    fileWithPath = fullfile( ...
                        self.dataPath, self.writeLogToDiskFileName);
                    topsDataLog.writeDataFile(fileWithPath);
                    self.nextWriteLogToDiskTime = ...
                        currentTime + self.writeLogToDiskInterval;
                end
            end
            self.runBriefly@dotsAllRemoteObjectManagers;
        end
        
        % Invoke compute() on a managed computable and get the result.
        % @param computable one of the dotsComputable objects in
        % managedObjects.
        % @param doPrepare true of false, whether to invoke
        % prepareToCompute() on @a computable, before invoking compute().
        % Defaults is false.
        % @deatils
        % Invokes the compute() method of @a computable and returns the
        % restult.  If doPrepare is true, also invokes preapareToCompute()
        % on @a computable, just before invoking compute().
        function result = mayComputeComputable(self, computable, doPrepare)
            if (nargin > 2) && doPrepare
                self.callObjectMethod(computable, 'prepareToCompute');
            end
            result = self.getObjectMethodResult(computable, ...
                'compute', 'computeResult');
        end
        
        % Prepare a group of computables to do (re)compute.
        % @param group string name of a managedObjects group
        % @details
        % Sets activeGroup for the client and server managers.  The objects
        % in the named @a group will be copied to activeComputables and
        % have its prepareToCompute() invoked.
        % @details
        % Subsequent calls to runBriedly() will invoke matComputenNow()
        % for each @a group object.
        function activateGroup(self, group)
            self.activeGroup = group;
            if self.clientMode && self.mayRequestTransaction;
                self.setObjectProperty( ...
                    self, 'activeGroup', group);
            end
        end
        
        % Account for an activated group.
        % @details
        % See activateGroup().
        function set.activeGroup(self, activeGroup)
            self.activeGroup = activeGroup;
            if self.managedObjects.containsGroup(activeGroup)
                objects = self.getGroup(activeGroup);
                self.activeComputables = objects;
                for ii = 1:length(objects)
                    objects{ii}.prepareToCompute;
                end
            else
                self.activeComputables = {};
            end
        end
        
        % Get recent data from topsDataLog.
        % @details
        % Gets a struct of data entries from topsDataLog, which are at
        % least as recent as latestLogUpdateTime.  Sets latestLogUpdateTime
        % to the latestTime of topsDataLog.
        % @details
        % Returns a struct array of recent log entries, with the same
        % format as topsDataLog.getSortedDataStruct().
        % @details
        % Also returns, as a second output, true or false, whether the log
        % has been flushed since the last getDataLogUpdate().
        function [dataStruct, logWasFlushed] = getDataLogUpdate(self)
            timeRange = [self.latestLogUpdateTime, inf];
            dataStruct = topsDataLog.getSortedDataStruct(timeRange);
            
            theLog = topsDataLog.theDataLog;
            logWasFlushed = ...
                self.latestLogUpdateTime < theLog.lastFlushTime;
            
            if isfinite(theLog.latestTime)
                self.latestLogUpdateTime = theLog.latestTime;
            else
                self.latestLogUpdateTime = theLog.lastFlushTime;
            end
        end
        
        % Add recent data to topsDataLog.
        % @param dataStruct struct array of recent log entries, with the
        % same format as topsDataLog.getSortedDataStruct()
        % @param doFlush optional true or false, whether to flush the
        % topsDataLog before adding @a dataStruct, default is false, do not
        % flush the log.
        % @details
        % Adds each log entry contained in dataStruct to the current
        % instance of topsDataLog.  If @a doFlush is true, flushes the log
        % before adding new data.
        % @details
        % Returns the length of the current topsDataLog instance, after
        % adding @a dataStruct.
        function logLength = applyDataLogUpdate(self, dataStruct, doFlush)
            if nargin > 2 && doFlush
                topsDataLog.flushAllData;
            end
            
            for ii = 1:numel(dataStruct)
                entry = dataStruct(ii);
                topsDataLog.logDataInGroup( ...
                    entry.item, entry.group, entry.mnemonic);
            end
            
            theLog = topsDataLog.theDataLog;
            logLength = theLog.length;
        end
    end
end